import gosu.Window;
import gosu.Button;
import gosu.Color;
import gosu.Image;

import org.jruby.*;
import org.jruby.runtime.*;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.javasupport.JavaUtil;
import org.jruby.anno.JRubyMethod;
import static org.jruby.RubyNumeric.*;


public class WindowProxy extends RubyObject {
  private static ObjectAllocator ALLOCATOR = new ObjectAllocator() {
    public IRubyObject allocate(Ruby runtime, RubyClass klass) {
      return new WindowProxy(runtime, klass);
    }
  };

  private Window _window;

  private final Ruby _runtime;

  private final RubyObject _self;

  private IRubyObject _textInput;

  public static void createWindowClass(Ruby runtime) {
    RubyModule mGosu = runtime.getModule("Gosu");
    RubyClass cWindow = mGosu.defineClassUnder("Window", runtime.getObject(), ALLOCATOR);

    cWindow.defineAnnotatedMethods(WindowProxy.class);

    defineGosuConstants(runtime, mGosu);
  }


  public WindowProxy(Ruby runtime, RubyClass type) {
    super(runtime, type);
    _runtime = runtime;
    _self = this;
  }

  /**
   * The initialize method for the Window JRuby object. 
   */
  @JRubyMethod(name = "initialize",
               required = 3,
               optional = 1,
               frame = true,
               visibility = Visibility.PRIVATE)
  public IRubyObject initialize(IRubyObject[] args) {
    int fps;
    if (Arity.checkArgumentCount(_runtime, args, 3, 4) == 3) {
      fps = 60;
    } else {
      fps = (int) (1000.0 / num2dbl(args[3]));
    }

    int width = fix2int(args[0]);
    int height = fix2int(args[1]);
    boolean fullscreen = args[2].isTrue();

    _window = new Window(width, height, fullscreen, fps) {
      public void update() {
        _self.callMethod(_runtime.getCurrentContext(), "update");
      }

      public void draw() {
        _self.callMethod(_runtime.getCurrentContext(), "draw");
      }

      public void buttonDown(Button button) {
        _self.callMethod(_runtime.getCurrentContext(), "button_down", _runtime.newFixnum(button.ordinal()));
      }

      public void buttonUp(Button button) {
        _self.callMethod(_runtime.getCurrentContext(), "button_up", _runtime.newFixnum(button.ordinal()));
      }
    };

    _textInput = _runtime.getNil();

    return this;
  }


  @JRubyMethod(name = "draw_line",
               required = 6,
               optional = 2)
  public IRubyObject drawLine(IRubyObject[] args) {
    int mode = Image.DEFAULT;
    double z = 0;

    if (args.length > 6) z = num2dbl(args[6]);
    if (args.length > 7) mode = getAlphaModeFrom(_runtime, args[7]);

    _window.drawLine(num2dbl(args[0]), num2dbl(args[1]), getColorFrom(args[2]),
                     num2dbl(args[3]), num2dbl(args[4]), getColorFrom(args[5]), z, mode);

    return _runtime.getNil();
  }


  @JRubyMethod(name = "draw_triangle",
               required = 9,
               optional = 2)
  public IRubyObject drawTriangle(IRubyObject[] args) {
    int mode = Image.DEFAULT;
    double z = 0;

    if (args.length > 9) z = num2dbl(args[9]);
    if (args.length > 10) mode = getAlphaModeFrom(_runtime, args[10]);

    _window.drawTriangle(num2dbl(args[0]), num2dbl(args[1]), getColorFrom(args[2]),
                         num2dbl(args[3]), num2dbl(args[4]), getColorFrom(args[5]),
                         num2dbl(args[6]), num2dbl(args[7]), getColorFrom(args[8]),
                         z, mode);

    return _runtime.getNil();
  }


  @JRubyMethod(name = "draw_quad",
               required = 12,
               optional = 2)
  public IRubyObject drawQuad(IRubyObject[] args) {
    int mode = Image.DEFAULT;
    double z = 0;

    if (args.length > 12) z = num2dbl(args[12]);
    if (args.length > 13) mode = getAlphaModeFrom(_runtime, args[13]);

    _window.drawQuad(num2dbl(args[0]), num2dbl(args[1]), getColorFrom(args[2]),
                     num2dbl(args[3]), num2dbl(args[4]), getColorFrom(args[5]),
                     num2dbl(args[6]), num2dbl(args[7]), getColorFrom(args[8]),
                     num2dbl(args[9]), num2dbl(args[10]), getColorFrom(args[11]),
                     z, mode);

    return _runtime.getNil();
  }


  @JRubyMethod(name = "width")
  public IRubyObject getWidth() {
    return _runtime.newFixnum(_window.getWidth());
  }


  @JRubyMethod(name = "height")
  public IRubyObject getHeight() {
    return _runtime.newFixnum(_window.getHeight());
  }


  @JRubyMethod(name = "show")
  public IRubyObject show() {
    _window.showWindow();
    return _runtime.getNil();
  }


  @JRubyMethod(name = "caption=",
               required = 1)
  public IRubyObject setCaption(IRubyObject caption) {
    _window.setCaption((String) JavaUtil.convertRubyToJava(caption, String.class));
    return _runtime.getNil();
  }


  @JRubyMethod(name = "caption")
  public IRubyObject caption() {
    return _runtime.newString(_window.caption());
  }


  /**
   * Does nothing. It is simply beautiful in its nothingness.
   */
  @JRubyMethod(name = "update")
  public IRubyObject updateStub() {
    return _runtime.getNil();
  }


  /**
   * Also does nothing.
   */
  @JRubyMethod(name = "draw")
  public IRubyObject drawStub() {
    return _runtime.getNil();
  }


  /**
   * Once again, nothing.
   */
  @JRubyMethod(name = "button_down",
               required = 1)
  public IRubyObject buttonDownStub(IRubyObject button) {
    return _runtime.getNil();
  }


  /**
   * Nothing here.
   */
  @JRubyMethod(name = "button_up",
               required = 1)
  public IRubyObject buttonUpStub(IRubyObject button) {
    return _runtime.getNil();
  }


  @JRubyMethod(name = "close")
  public IRubyObject close() {
    _window.close();
    return _runtime.getNil();
  }


  @JRubyMethod(name = "button_down?",
               required = 1)
  public IRubyObject isButtonDown(IRubyObject id) {
    return _runtime.newBoolean(_window.isButtonDown(Button.values()[fix2int(id)]));
  }


  @JRubyMethod(name = "mouse_x")
  public IRubyObject mouseX() {
    return _runtime.newFloat(_window.getMouseLocation().x);
  }


  @JRubyMethod(name = "mouse_y")
  public IRubyObject mouseY() {
    return _runtime.newFloat(_window.getMouseLocation().y);
  }


  @JRubyMethod(name = "button_id_to_char",
               required = 1)
  public IRubyObject buttonIdToChar(IRubyObject num) {
    return _runtime.newString(Character.toString(Button.values()[fix2int((RubyFixnum) num)].toChar()));
  }


  @JRubyMethod(name = "char_to_button_id",
               required = 1)
  public IRubyObject charToButtonId(IRubyObject c) {
    return _runtime.newFixnum(Button.getButton(((RubyString) c).toString().charAt(0)).ordinal());
  }


  @JRubyMethod(name = "text_input")
  public IRubyObject getTextInput() {
    return _textInput;
  }


  @JRubyMethod(name = "text_input=",
               required = 1)
  public IRubyObject setTextInput(IRubyObject arg) {
    _textInput = arg;
    if (arg.isNil()) {
      _window.setTextInput(null);
    } else {
      _window.setTextInput(((TextInputProxy) arg).getTextInput());
    }
    return arg;
  }


  @JRubyMethod(name = "clip_to",
               required = 4)
  public IRubyObject clipTo(ThreadContext context, IRubyObject[] args, Block block) {
    _window.beginClipping(fix2int(args[0]), fix2int(args[1]), fix2int(args[2]), fix2int(args[3]));
    block.yield(context, null);
    _window.endClipping();
    return _runtime.getNil();
  }


  public Window getWindow() { return _window; }


  protected static int getAlphaModeFrom(Ruby runtime, IRubyObject arg) {
    String alphaMode = ((RubySymbol) arg).asJavaString();
    if (alphaMode.equals("additive")) {
      return Image.ADDITIVE;
    } else if (alphaMode.equals("default")) {
      return Image.DEFAULT;
    } else {
      throw runtime.newArgumentError("invalid alpha mode");
    }
  }


  protected static Color getColorFrom(IRubyObject arg) {
    if (arg instanceof ColorProxy) {
      return ((ColorProxy) arg).getColor();
    }

    return new Color((int) fix2long(arg));
  }


  private static void defineGosuConstants(Ruby runtime, RubyModule mGosu) {
    for (Button button : Button.values()) {
      mGosu.defineConstant(button.toString(), runtime.newFixnum(button.ordinal()));
    }

    // For backwards compatibility: import the constants into Gosu::Button
    runtime.evalScriptlet("module Gosu::Button; Gosu.constants.each { |c| const_set(c, Gosu.const_get(c)) }; end");
  }
}
